今天要來換個redis寫法, 看看能不能縮短執行時間。
首先我會用git worktree
再開一個專案資料夾, 新資料夾就留在調整前, 稱為v1版, 再修改版本就是v2版。
git worktree 可以讓本機同時有兩個分支相互比較或進行開發, 有時候A分支功能寫到一半突然收到需求要改B分支的時候, 就需要先把分支commit或是做git stash, 後來改用worktree就可以直接再開一個資料夾對分支進行修改, 不會影響原本的開發進度; 另外像是這次要比較v1, v2 版本差異的時候, 就可以啟動兩個不同分支的版本進行比較。
使用 git worktree 開啟另一個專案
git worktree add -b ba_master ../coconut_2
修改 redis/limit.go
第二版採用 redis支援的lua-script方式修改, 寫好的整串lua-script是採原子式的方式排隊執行的, 要等到前一句script執行完才會執行下一個, 它可以保證script裡面的語法都執行完了才開始新的一組指令, 這在我們專案裡面很常使用到。
type LimitSetting struct {
Level1 int
Level2 int
Level3 int
}
// set point
func PointSetBatch(conn *redis.Client, keys []string, point int, limitSetting map[string]int, expired int) (err error) {
// LimitSetting , lua-script json decode 使用
tmp := &LimitSetting{
Level1: limitSetting["0"],
Level2: limitSetting["1"],
Level3: limitSetting["2"],
}
// script
luaScript := `
local point = tonumber(ARGV[1])
local limit = cjson.decode(ARGV[2])
local expired = tonumber(ARGV[3])
-- 先GET一次KEY, 沒有KEY的要SET, SET 同時要EXPIRE
if( redis.call('GET', KEYS[1]) == nil or redis.call('GET', KEYS[1]) == false) then
redis.call('SETEX', KEYS[1], expired, 0)
end
if( redis.call('GET', KEYS[2]) == nil or redis.call('GET', KEYS[2]) == false) then
redis.call('SETEX', KEYS[2], expired, 0)
end
if( redis.call('GET', KEYS[3]) == nil or redis.call('GET', KEYS[3]) == false) then
redis.call('SETEX', KEYS[3], expired, 0)
end
-- level 1
if(redis.call('GET', KEYS[1]) + point <= tonumber(limit.Level1)) then
redis.call('INCRBY',KEYS[1], point)
else
-- 定義不同的回傳值來區分踩到的限額是哪一個
return tostring(-99)
end
-- level 2
if(redis.call('GET', KEYS[2]) + point <= tonumber(limit.Level2)) then
redis.call('INCRBY',KEYS[2], point)
else
-- 定義不同的回傳值來區分踩到的限額是哪一個
return tostring(-98)
end
-- level 3
if(redis.call('GET', KEYS[3]) + point <= tonumber(limit.Level3)) then
redis.call('INCRBY',KEYS[3], point)
else
-- 定義不同的回傳值來區分踩到的限額是哪一個
return tostring(-97)
end
return 'ok'
`
script, err := conn.ScriptLoad(luaScript).Result()
if err != nil {
return err
}
reply, err := conn.EvalSha(script, keys, point, tmp.MarshalBinary(), expired).Result()
if err != nil {
return err
}
fmt.Println("reply:", reply)
// TODO: 處理 reply 回傳值對應資訊
return
}
// lua-script json decode 使用
func (s *LimitSetting) MarshalBinary() (ret string) {
data, _ := json.Marshal(s)
ret = string(data)
return ret
}
// lua-script json decode 使用
func (s *LimitSetting) UnmarshalBinary(data []byte) error {
return json.Unmarshal(data, s)
}
rpc.go 改打新的 function PointSetBatch
err = coconut_redis.PointSetBatch(s.RedisClient, keys, int(in.Point), limitSettings, 30)
if err != nil {
Logger.WithFields(map[string]interface{}{
"test": 111,
"time": time.Now().UnixNano(),
"err:": err.Error(),
}).Errorf("redis.PointSetBatch")
return nil, coconutError.ParseError(coconutError.ErrRedis, err)
}
測試v1, v2 版本差異
在不同的port啟動兩個不同分支的服務&不同台redis, 同時進行測試。
func main() {
// 連線到遠端 gRPC 伺服器。
conn1, err := grpc.Dial("localhost:3100", grpc.WithInsecure())
if err != nil {
log.Fatalf("conn 連線失敗:%v", err)
}
defer conn1.Close()
coco1 := coconut.NewCoconutClient(conn1)
// 連線到遠端 gRPC 伺服器。
conn2, err := grpc.Dial("localhost:3200", grpc.WithInsecure())
if err != nil {
log.Fatalf("conn 連線失敗:%v", err)
}
defer conn2.Close()
coco2 := coconut.NewCoconutClient(conn2)
n, _ := strconv.Atoi(os.Args[1])
var (
sum1 float64
sum2 float64
)
req := &coconut.PointsRequest{
Level_1: "aaa",
Level_2: "bbb",
Level_3: "ccc",
Point: 100,
}
wait := &sync.WaitGroup{}
for i := 0; i < n; i++ {
wait.Add(1)
go func() {
defer func() {
wait.Done()
}()
start := time.Now()
_, _ = coco1.UpdatePoints(context.Background(), req)
sum1 += time.Since(start).Seconds()
}()
}
for i := 0; i < n; i++ {
wait.Add(1)
go func() {
defer func() {
wait.Done()
}()
start := time.Now()
_, _ = coco2.UpdatePoints(context.Background(), req)
sum2 += time.Since(start).Seconds()
}()
}
wait.Wait()
fmt.Println("[v1] total count:", n, ", avg execute_time:", (sum1 / float64(n)))
fmt.Println("[v2] total count:", n, ", avg execute_time:", (sum2 / float64(n)))
}
測試 10次, 100次, 1000次的執行結果
經過多次分別測試10,100,1000的結果可以觀察到, 10次的時候兩個版本處理時間差異不大, 100~1000次就可以看出明顯的差異, 這樣就可以把版本調整為v2版了。